﻿// ----------------------------------------------------------------------
// <author name="Moses Kehengu"/>
// <copyright file="Modem.cs" company="Futures Group International">
//     Copyright statement. All right reserved
// </copyright>
//
// ------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GsmComm.GsmCommunication;
using GsmComm.PduConverter;
using GsmComm.PduConverter.SmartMessaging;
using System.Data;
using System.Data.Common;
using System.Threading;
using System.Windows.Forms;
using GenericTools;
using DataModels.Common;

namespace Reporting
{
    public class Modem
    {
        #region Class Variables
        private GsmCommMain comm;
        private int DBRecordID;
        private List<string> preferedPrefices;

        #region Modem Setup
        private string name;
        private string portname;
        private int baudRate;
        private int timeout;
        #endregion

        private bool saved;
        private bool canConnect;
        private Exception lastError;
        private bool canSendReply = true;

        private string lastPhoneNumber;
        private string lastMessageSent;
        #endregion

        #region Class Constants
        private const int MAX_SENDING_RETRY = 3;
        private const int BLOCKING_WARNING_LABEL = 66;
        private const int MAX_SMS_LENGTH = 160;

        #endregion

        #region Class Enumeration
        /// <summary>
        /// Types of messages
        /// </summary>
        private enum MessageType
        {
            /// <summary>
            /// Received SMS
            /// </summary>
            RECEIVED = 0,

            /// <summary>
            /// Sent SMS
            /// </summary>
            SENT = 1,

            /// <summary>
            /// Delivery Report
            /// </summary>
            DELIVERY_REPORT = 2
        }
        #endregion

        #region Constructors
        public Modem(GsmCommMain comm)
        {
            this.comm = comm;
            this.preferedPrefices = new List<string>();
            this.DBRecordID = -1;
            this.name = String.Empty;

            if (comm.IsConnected())
            {
                OperatorInfo operatorInfo = null;
                try
                {
                    operatorInfo = comm.GetCurrentOperator();
                    if (operatorInfo.Format == OperatorFormat.Numeric)
                    {
                        this.name = Utilities.GetModemServiceOperator(operatorInfo.TheOperator);
                    }
                    else this.name = operatorInfo.TheOperator;
                }
                catch
                {
                    this.name = Utilities.GetModemServiceOperator("N/A");
                } 
                this.canConnect = true;
            }
            else
            {
                this.canConnect = false;
            }
            this.portname = comm.PortName;
            this.baudRate = comm.BaudRate;
            this.timeout = comm.Timeout;
            this.saved = false;
        }

        public Modem(int DBSettingsID)
        {
            this.DBRecordID = DBSettingsID;
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"SELECT [ModemSettingNo], [ModemName], [PortName], [BaudRate], [WriteTimeout] 
                FROM [ModemSettings] WHERE [ModemSettingNo]=@ModemSettingNo";

            DbParameter param = command.CreateParameter();
            param.ParameterName = "ModemSettingNo";
            param.DbType = DbType.Int32;
            param.Value = DBSettingsID;
            command.Parameters.Add(param);

            DataTable dataTable = GenericDataAccess.ExecuteSelectCommand(command);
            if (dataTable.Rows.Count == 1)
            {
                DataRow row = dataTable.Rows[0];
                int settingID = Int32.Parse(row["ModemSettingNo"].ToString());
                string name = row["ModemName"].ToString();
                string port = row["PortName"].ToString();
                int rate = Int32.Parse(row["BaudRate"].ToString());
                int timeout = Int32.Parse(row["WriteTimeout"].ToString());

                GsmCommMain gsmComm = new GsmCommMain(port, rate, timeout);
                try
                {
                    gsmComm.Open();
                    if (gsmComm.IsConnected())
                    {
                        this.canConnect = true;
                        this.comm = gsmComm;
                        OperatorInfo operatorInfo = null;
                        try
                        {
                            operatorInfo = comm.GetCurrentOperator();
                            if (operatorInfo.Format == OperatorFormat.Numeric)
                            {
                                this.name = Utilities.GetModemServiceOperator(operatorInfo.TheOperator);
                            }
                            else this.name = operatorInfo.TheOperator;
                        }
                        catch
                        {
                            this.name = Utilities.GetModemServiceOperator("N/A");
                        } 
                    }
                    else
                    {
                        this.canConnect = false;
                    }
                    this.portname = comm.PortName;
                    this.baudRate = comm.BaudRate;
                    this.timeout = comm.Timeout;
                    this.DBRecordID = DBSettingsID;
                    this.saved = true;
                    this.preferedPrefices = new List<string>();
                    LoadPreferedPrefices();
                }
                catch (Exception ex)
                {
                    this.lastError = ex;
                    this.canConnect = false;
                }
            }
            else
            {
                this.portname = String.Empty;
                this.baudRate = 0;
                this.timeout = 0;
                this.saved = false;
                this.canConnect = false;
                this.DBRecordID = -1;
            }
        }

        private Modem()
        {
            this.comm = null;
            this.DBRecordID = -1;
            this.preferedPrefices = new List<string>();
            this.name = string.Empty;
            this.canConnect = false;
            this.portname = string.Empty;
            this.timeout = 0;
            this.saved = false;
        }
        #endregion

        #region Public Methods
        public void SaveInDB()
        {
            this.DBRecordID = SaveModemInDB(this.name, this.portname, this.baudRate, this.timeout);
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"INSERT INTO ModemAssociatedCellNumbers ([ModemSettingNo], 
                [AssociatedCellNum]) VALUES (@ModemID, @Prefix)";
            DbParameter modemID = command.CreateParameter();
            modemID.ParameterName = "@ModemID";
            modemID.DbType = DbType.Int32;
            modemID.Value = this.DBRecordID;
            
            foreach (string prefix in this.preferedPrefices)
            {
                command.Parameters.Clear();
                DbParameter param = command.CreateParameter();
                param.ParameterName = "@Prefix";
                param.DbType = DbType.String;
                param.Value = prefix;

                command.Parameters.Add(modemID);
                command.Parameters.Add(param);
                GenericDataAccess.ExecuteNonQuery(command, false);
            }
            if (command.Connection.State == ConnectionState.Open) command.Connection.Close();
        }

        public void RemoveInDB()
        {
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"DELETE FROM ModemAssociatedCellNumbers WHERE ModemSettingNo=@ModemNumberID";

            DbParameter param = command.CreateParameter();
            param.ParameterName = "@ModemNumberID";
            param.DbType = DbType.Int32;
            param.Value = this.DBRecordID;
            command.Parameters.Add(param);
            GenericDataAccess.ExecuteNonQuery(command, false);

            command.CommandText = @"DELETE FROM ModemSettings WHERE ModemSettingNo=@ModemNumberID";
            GenericDataAccess.ExecuteNonQuery(command);
            this.DBRecordID = -1;
        }

        public void SavePreferedPrefices()
        {
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;

            command.CommandText = @"DELETE FROM ModemAssociatedCellNumbers WHERE ModemSettingNo=@ModemNumberID";
            DbParameter param = command.CreateParameter();
            param.ParameterName = "@ModemNumberID";
            param.DbType = DbType.Int32;
            param.Value = this.DBRecordID;
            command.Parameters.Add(param);
            GenericDataAccess.ExecuteNonQuery(command, false);

            command.CommandText = @"INSERT INTO ModemAssociatedCellNumbers ([ModemSettingNo], 
                [AssociatedCellNum]) VALUES (@ModemID, @Prefix)";
            DbParameter modemID = command.CreateParameter();
            modemID.ParameterName = "@ModemID";
            modemID.DbType = DbType.Int32;
            modemID.Value = this.DBRecordID;

            foreach (string prefix in this.preferedPrefices)
            {
                command.Parameters.Clear();
                param = command.CreateParameter();
                param.ParameterName = "@Prefix";
                param.DbType = DbType.String;
                param.Value = prefix;

                command.Parameters.Add(modemID);
                command.Parameters.Add(param);
                GenericDataAccess.ExecuteNonQuery(command, false);
            }
            if (command.Connection.State == ConnectionState.Open) command.Connection.Close();
        }

        public bool SendSMS(string number, string message, bool hasError)
        {
            if (string.IsNullOrWhiteSpace(message)) return true;
            if (string.IsNullOrWhiteSpace(number)) return true;
            if (!canSendReply)
            {
                message = string.Format("MESSAGE WITHHELD: {0}", message);
                return true;
            }

            bool giveWarning = false;
            if (IsNumberBlocked(number, out giveWarning))
            {
                this.lastError = new Exception("Number is blocked");
                return true;
            }

            if (hasError && giveWarning)
            {
                string warningText = Utilities.GetTranslationText(number, BLOCKING_WARNING_LABEL);
                warningText = String.Format(warningText, AppConfiguration.MaximumErrorTolerance);
                if (!message.EndsWith(".")) message = String.Format("{0}.", message);
                message = String.Format("{0} {1}", message, warningText);
            }

#if DEBUG
#else
            if (!comm.IsConnected())
            {
                this.lastError = new Exception("No phone connected");
                return false;
            }
#endif

            try
            {
#if DEBUG
                MessageBox.Show(null,
                    String.Format("Sent SMS:\n[{0}]: {1}", number, message),
                    String.Format("{0}: Sent SMS [Debug Mode]", AppConfiguration.ApplicationName),
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
#else
                if (message.Length <= MAX_SMS_LENGTH)
                {
                    SmsSubmitPdu pdu = new SmsSubmitPdu(message, number);
                    this.comm.SendMessage(pdu);
                    this.lastError = null;
                }
                else
                {
                    OutgoingSmsPdu[] pdus = SmartMessageFactory.CreateConcatTextMessage(message, number);
                    if (pdus == null) return false;
                    else
                    {
                        bool notSent = true;
                        int retry = 0;
                        do
                        {
                            retry = 1;
                            try
                            {
                                foreach (OutgoingSmsPdu pdu in pdus) this.comm.SendMessage(pdu);
                                this.lastError = null;
                                notSent = false;
                            }
                            catch (Exception ex)
                            {
                                if (retry > MAX_SENDING_RETRY) throw ex;
                                else retry++;

                                try { this.comm.Close(); }
                                catch { }
                                Thread.Sleep(1000);
                                this.comm.Open();
                                if (this.comm.IsConnected()) continue;
                                else throw ex;
                            }
                        } while (notSent);
                    }
                }
#endif
                LogSMS(number, message, DateTime.Now, MessageType.SENT);
                this.lastMessageSent = message;
                this.lastPhoneNumber = number;
                return true;
            }
            catch (Exception ex)
            {
                this.lastError = ex;
                return false;
            }
        }

        public bool ReadMessages(string storage)
        {
#if DEBUG
            return true;
#else
            try
            {
                if (!comm.IsOpen()) comm.Open();
                if (!comm.IsConnected())
                {
                    int trials = 0;
                    bool connected = false;
                    do
                    {
                        try
                        {
                            try { comm.Close(); }
                            catch { }
                            Thread.Sleep(3000);
                            comm.Open();
                            if (comm.IsConnected())
                            {
                                connected = true;
                                break;
                            }
                        }
                        catch { }
                        trials++;
                    } while (trials != MAX_SENDING_RETRY);

                    if (!connected)
                    {
                        string message = String.Format("[{0}]: No phone connected. Tried to re-connect {1} times",
                            this.name, trials);
                        Exception exc = new Exception(message);
                        //ModemException ex = new ModemException(this, exc, message, ModemException.RELOAD_DRIVER);
                        ModemException ex = new ModemException(this, exc, message);
                        this.lastError = ex;
                        return false;
                    }
                }
            }
            catch (Exception ex)
            {
                string message = String.Format("[{0}]: {1}", this.name, ex.Message);
                //ModemException exc = new ModemException(this, ex, message, ModemException.RELOAD_DRIVER);
                ModemException exc = new ModemException(this, ex, message);
                this.lastError = exc;
                return false;
            }

            try
            {
                DecodedShortMessage[] messages = comm.ReadMessages(PhoneMessageStatus.All, storage);
                //TODO: Deal with other text encoding other than 7-bit encoding.

                foreach (DecodedShortMessage message in messages)
                {
                    SmsPdu inMessage = message.Data;
                    if (inMessage is SmsDeliverPdu)
                    {
                        SmsDeliverPdu data = (SmsDeliverPdu)inMessage;
                        LogSMS(data);
                        comm.DeleteMessage(message.Index, storage);
                    }
                }
                this.lastError = null;
                return true;
            }

            /*
            catch (InvalidOperationException ex)    // Communication thread is not running
            {
                try { comm.Close(); }
                catch { }
                try
                {
                    string port = comm.PortName;
                    int baudRate = comm.BaudRate;
                    int timeout = comm.Timeout;
                    comm = null;

                    comm = new GsmCommMain(port, baudRate, timeout);
                    comm.Open();
                    comm.IsConnected();
                    DecodedShortMessage[] messages = comm.ReadMessages(PhoneMessageStatus.All, storage);
                    foreach (DecodedShortMessage message in messages)
                    {
                        SmsPdu inMessage = message.Data;
                        if (inMessage is SmsDeliverPdu)
                        {
                            SmsDeliverPdu data = (SmsDeliverPdu)inMessage;
                            LogSMS(data);
                            comm.DeleteMessage(message.Index, storage);
                        }
                    }
                    this.lastError = null;
                    return true;
                }
                catch
                {
                    ModemException exc = new ModemException(this, ex, ex.Message, ModemException.RELOAD_DRIVER);
                    this.lastError = exc;
                    return false;
                }
            }

            catch (ArgumentException ex)
            {
                try { comm.Close(); }
                catch { }
                try
                {
                    string port = comm.PortName;
                    int baudRate = comm.BaudRate;
                    int timeout = comm.Timeout;
                    comm = null;

                    comm = new GsmCommMain(port, baudRate, timeout);
                    comm.Open();
                    comm.IsConnected();
                    DecodedShortMessage[] messages = comm.ReadMessages(PhoneMessageStatus.All, storage);
                    foreach (DecodedShortMessage message in messages)
                    {
                        SmsPdu inMessage = message.Data;
                        if (inMessage is SmsDeliverPdu)
                        {
                            SmsDeliverPdu data = (SmsDeliverPdu)inMessage;
                            LogSMS(data);
                            comm.DeleteMessage(message.Index, storage);
                        }
                    }
                    this.lastError = null;
                    return true;
                }
                catch
                {
                    ModemException exc = new ModemException(this, ex, ex.Message, ModemException.RELOAD_DRIVER);
                    this.lastError = exc;
                    return false;
                }
            }*/

            catch (Exception ex)
            {
                //ModemException exc = new ModemException(this, ex, ex.Message, ModemException.RELOAD_DRIVER);
                ModemException exc = new ModemException(this, ex, ex.Message);
                this.lastError = exc;
                return false;
            }
#endif
        }

        public void Close()
        {
            try
            {
                this.comm.Close();
            }
            catch { }
        }

        public IdentificationInfo IdentifyDevice()
        {
            return this.comm.IdentifyDevice();
        }

        #endregion

        #region Public Static Methods
        public static int AddModem(string modemName, string portName, int baudRate, 
            int timeout, bool checkConnection = false)
        {
            if (checkConnection)
            {
                try
                {
                    GsmCommMain comm = new GsmCommMain(portName, baudRate, timeout);
                    comm.Open();
                    if (!comm.IsConnected()) return -1;
                    comm.Close();
                }
                catch
                {
                    return -1;
                }
            }
            int id = SaveModemInDB(modemName, portName, baudRate, timeout);
            return id;
        }

        public static int AddModem(GsmCommMain communication)
        {
            if (communication.IsOpen())
            {
                string modemName = null;
                try
                {
                    OperatorInfo operatorInfor = communication.GetCurrentOperator();
                    if (operatorInfor.Format == OperatorFormat.Numeric)
                    {
                        modemName = Utilities.GetModemServiceOperator(operatorInfor.TheOperator);
                    }
                    else
                    {
                        modemName = operatorInfor.TheOperator;
                    }
                }
                catch
                {
                    modemName = Utilities.GetModemServiceOperator("N/A");
                }
                string portName = communication.PortName;
                int baudRate = communication.BaudRate;
                int timeout = communication.Timeout;
                int id = AddModem(modemName, portName, baudRate, timeout, false);
                return id;
            }
            return -1;
        }

        public static Modem CreateDebuggingModem()
        {
            Modem modem = null;
#if DEBUG
            modem = new Modem();
            modem.name = "DEBUGGING MODEM";
            modem.canConnect = true;
            modem.LoadPreferedPrefices();
#endif
            return modem;
        }

        #endregion

        #region Public Properties
        public List<string> PreferedPrefices
        {
            get
            {
                return this.preferedPrefices;
            }
        }

        public GsmCommMain Communication
        {
            get
            {
                return this.comm;
            }
        }

        public Exception LastError
        {
            get
            {
                return this.lastError;
            }
        }

        public bool CanConnect
        {
            get
            {
                return this.canConnect;
            }
        }

        public string PortName
        {
            get
            {
                return comm.PortName;
            }
        }

        public string Name
        {
            get
            {
                return this.name;
            }
        }

        public string LastPhoneNumber
        {
            get
            {
                return this.lastPhoneNumber;
            }
        }

        public string LastSentMessage
        {
            get
            {
                return this.lastMessageSent;
            }
        }
        #endregion

        #region Private Helper Methods
        #region Static Methods
        private static void LogSMS(string cellnumber, string text, DateTime time, MessageType type)
        {
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"INSERT INTO [SMSLog] ([SMSType],[CellNum],[Message],[Date],[Processed]) 
                VALUES(@SMSType, @CellNum, @Message, @Date, 'FALSE')";

            DbParameter param = command.CreateParameter();
            param.ParameterName = "@SMSType";
            param.DbType = DbType.Boolean;
            param.Value = type;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@CellNum";
            param.DbType = DbType.String;
            param.Value = cellnumber;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@Message";
            param.DbType = DbType.String;
            param.Value = text;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@Date";
            param.DbType = DbType.DateTime;
            param.Value = time;
            command.Parameters.Add(param);

            GenericDataAccess.ExecuteNonQuery(command);
        }

        private static void LogSMS(SmsPdu data)
        {
            if (data is SmsDeliverPdu)
            {
                SmsDeliverPdu message = (SmsDeliverPdu)data;
                string number = message.OriginatingAddress;
                string text = message.UserDataText;
                DateTime timeSent = message.SCTimestamp.ToDateTime();
                MessageType type = MessageType.RECEIVED;
                LogSMS(number, text, timeSent, type);
            }

            if (data is SmsSubmitPdu)
            {
                SmsSubmitPdu message = (SmsSubmitPdu)data;
                string number = message.DestinationAddress;
                string text = message.UserDataText;
                DateTime timeSent = DateTime.Now;
                MessageType type = MessageType.SENT;
                LogSMS(number, text, timeSent, type);
            }
        }

        private static int SaveModemInDB(string modemName, string portName, int baudRate, int timeout)
        {
            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"INSERT INTO ModemSettings(ModemName, PortName, BaudRate, ReadTimeout, WriteTimeout) 
                VALUES (@ModemName, @PortName, @BaudRate, @Timeout, @Timeout)";

            DbParameter param = command.CreateParameter();
            param.ParameterName = "@ModemName";
            param.DbType = DbType.String;
            param.Value = modemName;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@PortName";
            param.DbType = DbType.String;
            param.Value = portName;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@BaudRate";
            param.DbType = DbType.Int32;
            param.Value = baudRate;
            command.Parameters.Add(param);

            param = command.CreateParameter();
            param.ParameterName = "@Timeout";
            param.DbType = DbType.Int32;
            param.Value = timeout;
            command.Parameters.Add(param);
            GenericDataAccess.ExecuteNonQuery(command, false);

            command.CommandText = @"SELECT @@IDENTITY AS [InsertID]";
            string output = GenericDataAccess.ExecuteScalar(command);
            int DBRecordID = Int32.Parse(output);
            return DBRecordID;
        }        

        private static bool IsNumberBlocked(string number, out bool giveWarning)
        {
            if (Utilities.IsNumberBlocked(number))
            {
                giveWarning = false;
                return true;
            }

            DbCommand command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"SELECT COUNT(SenderNum) FROM ErrMsgSMS WHERE SenderNum=@SenderNum AND Deleted=0";
            DbParameter param = command.CreateParameter();
            param.ParameterName = "@SenderNum";
            param.DbType = DbType.String;
            param.Value = number;
            command.Parameters.Add(param);
            string output = GenericDataAccess.ExecuteScalar(command);
            int count = Int32.Parse(output);
            
            if (count == AppConfiguration.MaximumErrorTolerance) giveWarning = true;
            else giveWarning = false;
            
            bool blocked = (count > AppConfiguration.MaximumErrorTolerance);
            return blocked;
        }


        #endregion

        private void LoadPreferedPrefices()
        {
            DbCommand command = null;
#if DEBUG
            command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"SELECT [AssociatedCellNum] FROM [ModemAssociatedCellNumbers]";
#else
            if (this.DBRecordID == -1) return;
            command = GenericDataAccess.CreateCommand();
            command.CommandType = CommandType.Text;
            command.CommandText = @"SELECT [AssociatedCellNum] FROM [ModemAssociatedCellNumbers] 
                WHERE [ModemSettingNo]=@ModemSettingNo";

            DbParameter param = command.CreateParameter();
            param.ParameterName = "@ModemSettingNo";
            param.DbType = DbType.Int32;
            param.Value = this.DBRecordID;
            command.Parameters.Add(param);
#endif

            DataTable dataTable = GenericDataAccess.ExecuteSelectCommand(command);
            foreach (DataRow row in dataTable.Rows)
            {
                string prefix = row["AssociatedCellNum"].ToString();
                this.preferedPrefices.Add(prefix);
            }
        }
        #endregion
    }
}
